<script setup>
import {ref, onBeforeUnmount, computed, reactive, nextTick} from 'vue'
import Text from './text.vue';
import Recorder from './recorder.vue'
import Media from './Media.vue';
import {useMove} from './useMove.js'
import {formatTime} from './index'
import Refrence from './refrence.vue'

const visible = ref(false)
const isMaximize = ref(false)
const isCollapse = ref(false)
const userInfo = ref({})
const userList = ref([])
const chatMap = reactive(new Map())
const msg = ref('')
const text = ref('')
const target = ref('')
const newMessage = reactive(new Set())
const record = ref()
const wnd = ref()
const chat = ref()
const user = ref()
const emoji = [
    '😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','🥰','😗','😙','😚','🙂',
    '🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','🥱','😴','😌',
    '😛','😔','😓','😤','😱','🤪','😭','😷','🤧']
const isEmojiOpen = ref(false)
const visibleRecorder = ref(false)
const visibleMedia = ref(false)
const media = ref('')
const mediaConfig = ref({})
let localStream = null;
const {left, top, width, height} = useMove()


const emit = defineEmits(['onmessage'])

const msgList = computed(() => {
    if(chatMap.has(target.value)) return chatMap.get(target.value).msgList
    return []
})
const userName = computed(() => {
    if(target.value) {
        const user = userList.value.find(v => v.id === target.value)
        return user ? user.name : ''
    }
    return ''
})
const props = defineProps({
    config: {
        type: Object,
        default: () => ({})
    }
})
const ws = new WebSocket(props.config.ws)
const messageType = {
    user_info(data) {
        userInfo.value = data.userInfo
    },
    user_list(data) {
        userList.value = data.result
    },
    offer(data) {
        createConnect(data.from, data.offer)
    },
    re_offer(data) {
        const {pc} = chatMap.get(data.from)
        pc.setRemoteDescription(new RTCSessionDescription(data.offer)).then(() => {
            if(localStream) localStream.getTracks().forEach(track => pc.addTrack(track))
            pc.createAnswer().then(answer => {
                send({type: 'answer', answer, to: data.from})
                pc.setLocalDescription(answer)
            })
        })
    },
    request_refused(data) {
        mediaConfig.value.type = 'request_refused'
    },
    request_cancel(data) {
        mediaConfig.value.type = 'request_cancel'
    },
    request_video(data) {
        if(chatMap.has(data.from)) {
            visibleMedia.value = true
            mediaConfig.value.type = 'request_video'
            mediaConfig.value.userInfo = getUserInfo(data.from)
        }
    },
    request_voice(data) {
        if(chatMap.has(data.from)) {
            visibleMedia.value = true
            mediaConfig.value.type = 'request_voice'
            mediaConfig.value.userInfo = getUserInfo(data.from)
        }
    },
    over_talk(data) {
        media.value.over()
        if(localStream) {
            localStream.getTracks().forEach(track => track.stop())
            localStream = null
        }
    },
    answer(data) {
        const {pc} = chatMap.get(data.from)
        pc.setRemoteDescription(new RTCSessionDescription(data.answer))
    },
    candidate(data) {
        const {pc} = chatMap.get(data.from)
        pc.addIceCandidate(new RTCIceCandidate(data.canidate))
    }
}
function send(data) {
    ws.send(JSON.stringify(data))
}

function scrollBottom() {
    record.value.scrollTo(0, record.value.scrollHeight)
}

function closeEmoji() {
    isEmojiOpen.value = false
    document.removeEventListener('click', closeEmoji)
}

function openEmoji() {
    isEmojiOpen.value = !isEmojiOpen.value
    setTimeout(() => {
        document.addEventListener('click', closeEmoji)
    })
}

function blobToArray(b) {
    return b.arrayBuffer().then(buff => {
        return Array.from(new Uint8Array(buff))
    })
}

const bufferSize = 2 * 1024 * 1024
const sendFile = {}
async function sendChunk(index, total, file, chunkSize, type, id, dc, obj, time) {
    if(!sendFile[id]) return
    if(dc.bufferedAmount > bufferSize) return setTimeout(() => {
        sendChunk(index, total, file, chunkSize, type, id, dc, obj, time)
    })
    const isEnd = (index + 1) === total;
    obj.value.isEnd = isEnd
    obj.value.index = index + 1
    obj.value.total = total
    const data = {
        id: userInfo.value.id,
        name: userInfo.value.name,
        userHead: userInfo.value.userHead,
        type,
        index,
        total,
        isEnd,
        time,
        fileName: file.name,
        fileType: file.type,
        msgId: id,
        chunk: await blobToArray(file.slice(index * chunkSize, (index + 1) * chunkSize))
    }
    dc.send(JSON.stringify(data))
    isEnd && delete sendFile[id]
    if(!isEnd) setTimeout(() => {
        sendChunk(index + 1, total, file, chunkSize, type, id, dc, obj, time)
    })
}

function getType(v) {
    if(/jpeg|jpg|png|gif|bmp|webp|svg/i.test(v)) return 'image'
    if(/mp4/i.test(v)) return 'video'
    return 'other'
}

function genId() {
    let s = Date.now().toString(16)
    while(s.length < 32) {
        s += Math.random().toString(16).substring(2)
    }
    return s.substring(0, 32)
}
const chunkSize = 50 * 1024

function openFile() {
    const input = document.createElement('input')
    input.type = 'file'
    input.onchange = () => {
        if(input.files.length === 0) return
        const file = input.files[0]
        const count = Math.ceil(file.size / chunkSize)
        const type = getType(file.type)
        const id = genId()
        const {dc, msgList} = chatMap.get(target.value)
        textFocus()
        const obj = {
            id: userInfo.value.id,
            name: userInfo.value.name,
            userHead: userInfo.value.userHead,
            type,
            fileName: file.name,
            url: window.URL.createObjectURL(file),
            progress: ref({}),
            msgId: id,
            createTime: Date.now()
        }
        sendFile[id] = true
        sendChunk(0, count, file, chunkSize, type, id, dc, obj.progress)
        insertTime(target.value)
        msgList.push(obj)
    }
    const evt = new MouseEvent('click')
    input.dispatchEvent(evt)
}

function openRecord() {
    visibleRecorder.value = true
}

function sendVoice(blob, time) {
    const file = blob;
    const count = Math.ceil(file.size / chunkSize)
    const type = 'voice'
    const id = genId()
    const {dc, msgList} = chatMap.get(target.value)
    textFocus()
    const obj = {
        id: userInfo.value.id,
        name: userInfo.value.name,
        userHead: userInfo.value.userHead,
        type,
        time,
        fileName: file.name,
        url: window.URL.createObjectURL(file),
        progress: ref({}),
        msgId: id,
        createTime: Date.now()
    }
    sendFile[id] = true
    sendChunk(0, count, file, chunkSize, type, id, dc, obj.progress, time)
    insertTime(target.value)
    msgList.push(obj)
}

function insertEmoji(v) {
    text.value.focus()
    const start = text.value.selectionStart
    const end = text.value.selectionEnd
    msg.value = msg.value.substring(0, start) + v + msg.value.substring(end)
    nextTick(() => {
        if(start === end) {
            text.value.selectionStart = end + v.length
            text.value.selectionEnd = end + v.length
        } else {
            text.value.selectionStart = start + v.length
            text.value.selectionEnd = start + v.length
        }
    })
    closeEmoji()
}

function textFocus() {
    nextTick(() => {
        text.value && text.value.focus()
    })
}

function insertTime(id) {
    if(chatMap.has(id)) {
        const {msgList} = chatMap.get(id)
        if(msgList.length === 0 || Date.now() - msgList[msgList.length - 1].createTime > 120000)
            msgList.push({
                type: 'time',
                time: formatTime('HH:mm')
            })
    }
}

function backspace(e) {
    if(e.key === 'Backspace' && !msg.value) refrenceMap.value = null
}

function sendMessage() {
    textFocus()
    msg.value = msg.value.replace(/^\s*|\s*$/g, '')
    if(!msg.value || !chatMap.has(target.value)) return;
    const {dc, msgList} = chatMap.get(target.value)
    let data = {
        type: 'text',
        id: userInfo.value.id,
        msgId: genId(),
        name: userInfo.value.name,
        userHead: userInfo.value.userHead,
        text: msg.value,
        createTime: Date.now()
    }
    msg.value = ''
    if(refrenceMap.value) {
        data.refrence = refrenceMap.value
        refrenceMap.value = null
    }
    dc.send(JSON.stringify(data))
    insertTime(target.value)
    msgList.push(data)
}
ws.onmessage = e => {
    const data = JSON.parse(e.data)
    messageType[data.type] && messageType[data.type](data)
}

function refused() {
    send({type: 'request_refused', to: target.value})
}

function requestCancel() {
    send({type: 'request_cancel', to: target.value})
    if(localStream) {
        localStream.getTracks().forEach(track => track.stop())
        localStream = null
    }
}

function getUserInfo(id) {
    return userList.value.find(v => v.id === id)
}

function requestVoice() {
    navigator.mediaDevices.getUserMedia({audio: true}).then(stream => {
        localStream = stream
        send({type: 'request_voice', to: target.value})
        visibleMedia.value = true
        mediaConfig.value.type = 'voice'
        mediaConfig.value.userInfo = getUserInfo(target.value)
    })
}

function requestVideo() {
    navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(stream => {
        localStream = stream
        send({type: 'request_video', to: target.value})
        visibleMedia.value = true
        mediaConfig.value.type = 'video'
        mediaConfig.value.userInfo = getUserInfo(target.value)
    })
}

function requestAccept({type, id}) {
    let config = type === 'request_voice' ? {audio: true} : {video: true, audio: true}
    navigator.mediaDevices.getUserMedia(config).then(stream => {
        localStream = stream
        const {pc} = chatMap.get(id)
        localStream.getTracks().forEach(track => pc.addTrack(track))
        pc.createOffer().then(offer => {
            send({type: 're_offer', to: target.value, offer})
            pc.setLocalDescription(offer)
        })
    }).catch(() => {
        mediaConfig.value.type = 'denied'
    })
}

function overTalk(id) {
    send({type: 'over_talk', to: id})
    if(localStream) {
        localStream.getTracks().forEach(track => track.stop())
        localStream = null
    }
}

function withdraw(msgId) {
    const {msgList, dc} = chatMap.get(target.value)
    const index = msgList.findIndex(v => v.msgId === msgId)
    if(index > -1) {
        const msg = msgList.splice(index, 1, {
            type: 'withdraw',
            text: '你撤回了一条消息'
        })
        if(msg[0].url) window.URL.revokeObjectURL(msg[0].url)
        delete sendFile[msgId]
        dc.send(JSON.stringify({
            type: 'withdraw',
            msgId,
            name: userInfo.value.name
        }))
    }
}
const refrenceMap = ref(null)
function refrence(msg) {
    refrenceMap.value = msg
}

function createConnect(id, offer) {
    if(!offer) target.value = id
    textFocus()
    newMessage.delete(id)
    refrenceMap.value = null
    if(chatMap.has(id)) return 
    const pc = new RTCPeerConnection({
        iceServers: [
            {"urls": "stun:stun.l.google.com:19302"}
        ]
    })
    const dc = pc.createDataChannel('x', {negotiated: true, id: 0, ordered: true})
    chatMap.set(id, {pc, dc, msgList: []})
    dc.onopen = () => {
    }
    let temp = {}
    dc.onmessage = e => {
        const data = JSON.parse(e.data)
        const {msgList} = chatMap.get(id)
        if(data.type === 'withdraw') {
            const index = msgList.findIndex(v => v.msgId === data.msgId)
            if(index > -1) {
                const msg = msgList.splice(index, 1, {
                    type: 'withdraw',
                    text: data.name +' 撤回了一条消息'
                })
                if(msg[0].url) window.URL.revokeObjectURL(msg[0].url)
            } else {
                if(temp[data.msgId]) {
                    delete temp[data.msgId]
                }
            }
            return;
        }
        if(data.chunk) {
            if(!temp[data.msgId]) temp[data.msgId] = new Array(data.total)
            temp[data.msgId][data.index] = new Uint8Array(data.chunk);
            if(data.isEnd) {
                const work = new Worker(new URL('./file.js', import.meta.url))
                work.postMessage({chunks: temp[data.msgId], filename: data.fileName, type: data.fileType})
                work.onmessage = e => {
                    let re = {
                        type: data.type, 
                        id: data.id,
                        msgId: data.msgId,
                        name: data.name,
                        userHead: data.userHead,
                        fileName: data.fileName,
                        time: data.time,
                        url: window.URL.createObjectURL(e.data),
                        createTime: Date.now()
                    }
                    insertTime(id)
                    msgList.push(re)
                    emit('onmessage', re)
                }
                delete temp[data.msgId]
            } else return;
        } else {
            insertTime(id)
            msgList.push(data)
            emit('onmessage', data)
        }
        if(target.value !== id) {
            newMessage.add(id)
        }
    }
    dc.onclose = () => {
        chatMap.get(id).msgList.forEach(v => {
            v.url && window.URL.revokeObjectURL(v.url)
        })
        chatMap.delete(id)
        target.value = null
    }
    pc.onconnectionstatechange = e => {
        if (e.target.connectionState === 'disconnected' ||
            e.target.connectionState === 'failed') {
            pc.close()
        }
    }
    pc.onicecandidate = e => {
        e.candidate && send({type: 'candidate', to: id, canidate: e.candidate})
    }
    let stream = null
    pc.ontrack = e => {
        if(!stream) {
            stream = new MediaStream()
            media.value.setStream(stream, () => stream = null)
        }
        stream.addTrack(e.track)
    }
    if(offer) {
        return pc.setRemoteDescription(new RTCSessionDescription(offer)).then(() => {
            pc.createAnswer().then(answer => {
                send({type: 'answer', answer, to: id})
                pc.setLocalDescription(answer)
            })
        })
    }
    pc.createOffer().then(offer => {
        send({type: 'offer', offer, to: id})
        pc.setLocalDescription(offer)
    })
}

function connect(id) {
    createConnect(id)
}

function show() {
    visible.value = true
}

function hide() {
    visible.value = false
}

function minimize() {
    isCollapse.value = true
}

function maximize() {
    isMaximize.value = true
}

function resotre() {
    isMaximize.value = false
}
let resizeWnd = {x: 0, y: 0}, wndSize = 400, mvFn
function resizeMV(e) {
    mvFn(e.clientX - resizeWnd.x, e.clientY - resizeWnd.y)
}
function resizeUP() {
    document.removeEventListener('mousemove', resizeMV)
    document.removeEventListener('mouseup', resizeUP)
}
function initEvent(e) {
    e.preventDefault()
    resizeWnd.x = e.clientX
    resizeWnd.y = e.clientY
    document.addEventListener('mousemove', resizeMV)
    document.addEventListener('mouseup', resizeUP)
}
function initWnd() {
    if(width.value) return
    const rect = wnd.value.getBoundingClientRect()
    left.value = rect.left
    top.value = rect.top
    width.value = rect.width
    height.value = rect.height
}
function resizeE(e) {
    initEvent(e)
    initWnd()
    const w = width.value
    mvFn = (dx, dy) => {
        width.value = Math.max(wndSize, w + dx)
    }
}
function resizeN(e) {
    initEvent(e)
    initWnd()
    resizeWnd.t = top.value + (height.value - wndSize)
    const h = height.value, t = top.value
    mvFn = (dx, dy) => {
        height.value = Math.max(wndSize, h - dy)
        top.value = t + dy
        if(height.value === wndSize) top.value = resizeWnd.t
    }
}
function resizeChat(e) {
    initEvent(e)
    const rect = chat.value.getBoundingClientRect()
    mvFn = (dx, dy) => {
        let h = Math.max(rect.height + dy, 100)
        chat.value.style.height = h + 'px'
    }
}
function resizeUser(e) {
    initEvent(e)
    initWnd()
    const rc = user.value.getBoundingClientRect()
    mvFn = (dx, dy) => {
        let w = Math.min(width.value - 250, Math.max(rc.width + dx, 100))
        user.value.style.width = w + 'px'
    }
}

onBeforeUnmount(() => {
    ws.close()
})

defineExpose({
    show, hide
})
</script>

<template>
    <teleport to="body">
        <div class="c-webrtc" v-if="visible" :class="{minimize: isCollapse}">
            <div ref="wnd" v-if="!isCollapse" class="c-webrtc-content" :class="{maximize: isMaximize}"  
            :style="{width:width + 'px', height: height + 'px', left: left + 'px', top: top + 'px'}">
                <div class="c-webrtc-user-list" ref="user">
                    <div v-for="k in userList" @click="connect(k.id)" :class="{new:newMessage.has(k.id),selected:target===k.id}">
                        <img :src="k.userHead" class="c-webrtc-user-head">
                        <div>{{ k.name }}</div>
                    </div>
                    <div class="c-webrtc-resize-e" @mousedown="resizeUser"></div>
                </div>
                <div class="c-webrtc-chat">
                    <div class="c-webrtc-chat-content" ref="chat">
                        <div class="c-webrtc-chat-menu">
                            <div class="c-webrtc-chat-menu-username">{{ userName }}</div>
                            <div>
                                <span class="iconfont icon-zuixiaohua" @click="minimize"></span>
                                <span v-if="!isMaximize" class="iconfont icon-zuidahua" @click="maximize"></span>
                                <span v-else class="iconfont icon-chuangkouhuanyuan" @click="resotre"></span>
                                <span class="iconfont icon-guanbi" @click="hide"></span>
                            </div>
                        </div>
                        <div class="c-webrtc-chat-record" v-if="target" ref="record">
                            <template v-for="k in msgList">
                                <slot name="message" :message="k" :sender="k.id===userInfo.id">
                                    <div class="c-webrtc-chat-message" :class="{sender:k.id===userInfo.id}">
                                    <div v-if="k.type==='time'" class="c-webrtc-time">{{ k.time }}</div>
                                    <div v-else-if="k.type==='withdraw'" class="c-webrtc-time">{{ k.text }}</div>
                                    <template v-else>
                                        <img :src="k.userHead" class="c-webrtc-chat-user-head">
                                        <Text :msg="k" 
                                            @scroll-bottom="scrollBottom" 
                                            @withdraw="withdraw"
                                            @refrence="refrence"
                                            :id="userInfo.id"></Text>
                                    </template>
                                </div>
                                </slot>
                            </template>
                        </div>
                    </div>
                    <div class="c-webrtc-chat-text" v-if="target">
                        <div class="c-webrtc-chat-text-menu">
                            <div>
                                <span title="表情" class="iconfont icon-biaoqing" @click.stop="openEmoji"></span>
                                <span title="文件" class="iconfont icon-wenjianjia" @click="openFile"></span>
                                <span title="录音" class="iconfont icon-yuyin" @click="openRecord"></span>
                                <div class="c-webrtc-emoji" v-if="isEmojiOpen">
                                    <div v-for="k in emoji" @click="insertEmoji(k)">{{ k }}</div>
                                </div>
                            </div>
                            <div>
                                <span title="语音通话" class="iconfont icon-yuyintonghua" @click="requestVoice"></span>
                                <span title="视频通话" class="iconfont icon-shipintonghua" @click="requestVideo"></span>
                            </div>
                            <div class="c-webrtc-resize-n" @mousedown="resizeChat"></div>
                        </div>
                        <div v-if="refrenceMap">
                            <Refrence :msg="refrenceMap"></Refrence>
                        </div>
                        <textarea @keyup.ctrl.enter="sendMessage" v-model="msg" ref="text" @keydown="backspace"></textarea>
                        <div>
                            <button @mousedown.stop @click="sendMessage">发送(Ctrl + Enter)</button>
                        </div>
                    </div>
                </div>
                <div class="c-webrtc-resize-n" @mousedown="resizeN"></div>
                <div class="c-webrtc-resize-e" @mousedown="resizeE"></div>
            </div>
            <div v-else @click="isCollapse=false">
                <slot name="minimize">
                    <span class="iconfont icon-xiaoxi"></span>
                </slot>
            </div>
        </div>
        <Recorder v-model="visibleRecorder" @voice="sendVoice"/>
        <Media v-model="visibleMedia" ref="media" :config="mediaConfig" 
            @refused="refused" 
            @cancel="requestCancel" 
            @accept="requestAccept" 
            @over="overTalk"/>
    </teleport>
</template>